package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-08-04 09:06
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {


    /**
     * 商品索引库名称
     */
    private static final String INDEX_NAME = "goods";

    @Autowired
    private ProductFeignClient productFeignClient;

    //默认链路追踪包含线程池对象
    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;


    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 将来真正实现索引库导入
     * 将指定商品SKU封装商品文档对象，保存到ES索引库
     * 封装Goods对象为相关属性赋值，将Goods存入ES
     *
     * @param skuId
     * @return
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.创建索引库文档对象 Goods
            Goods goods = new Goods();
            //1.1 远程调用商品服务获取商品SKU基本信息(可能读Redis缓存)-封装Goods中商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo != null) {
                    goods.setId(skuId);
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                    goods.setCreateTime(skuInfo.getCreateTime());
                    goods.setCreatedDate(skuInfo.getCreateTime());
                }
                return skuInfo;
            }, threadPoolExecutor);

            CompletableFuture<Void> priceCOmCompletableFuture = CompletableFuture.runAsync(() -> {
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                if (skuPrice != null) {
                    goods.setPrice(skuPrice.doubleValue());
                }
            }, threadPoolExecutor);

            //1.2 远程调用商品服务获取品牌信息-封装Goods中品牌信息
            CompletableFuture<Void> baseTrademartCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);

            //1.3 远程调用商品服务获取分类信息-封装Goods中分类信息
            CompletableFuture<Void> categoryViewCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());
                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());
                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }, threadPoolExecutor);

            //1.4 远程调用商品服务获取平台属性列表-封装Goods中平台属性信息
            CompletableFuture<Void> attrListCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    //将BaseAttrInfo集合转为SearchAttr集合
                    List<SearchAttr> searchAttrList = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);

            //1.5 汇总所有异步任务执行结果
            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCOmCompletableFuture,
                    categoryViewCompletableFuture,
                    baseTrademartCompletableFuture,
                    attrListCompletableFuture
            ).join();

            //2.将Goods保存到索引库-查询ES提供JavaClient调用SDK方法完成导入
            //2.1 构建创建文档请求对象 封装索引库 // PUT /<index>/_doc/<_id> = PUT goods/_doc/24
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME).id(skuId.toString());
            //2.2 为请求对象设置请求体参数（文档对象JSON）

            String goodsJSON = JSON.toJSONString(goods);
            indexRequest.source(goodsJSON, XContentType.JSON);

            //2.3 执行保存索引库
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("[搜索服务]新增商品文档异常：{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 测试接口：将指定商品下架
     *
     * @param skuId
     * @return
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            //1.构建删除文档请求对象
            DeleteRequest request = new DeleteRequest(
                    INDEX_NAME,
                    skuId.toString());
            //2.执行删除
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]下架商品异常：{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 对指定商品分值（热门商品）进行更新
     *
     * @param skuId
     * @param score
     */
    @Override
    public void incrHotScore(Long skuId, int score) {
        try {
            //1.对存放到Redis中商品热榜进行修改分值
            String hotkey = "hot:goods:score";
            Double newScore = redisTemplate.opsForZSet().incrementScore(hotkey, skuId.toString(), score);
            //2.对ES中分值进行修改（对ES写操作进行“稀释”）
            if (newScore % 10 == 0) {
                //2.1 构建修改文档请求对象
                UpdateRequest updateRequest = new UpdateRequest(INDEX_NAME, skuId.toString());
                //2.2 修改内容
                Goods goods = new Goods();
                goods.setId(skuId);
                goods.setHotScore(newScore.longValue());
                updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);

                //2.3 执行ES文档修改操作
                restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
            }
        } catch (Exception e) {
            log.error("[搜索服务]，修改商品分值异常：{}", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 商品检索，包含：分页检索商品信息，动态聚合过滤条件。支持多条件过滤
     * 检索功能三大步骤：
     * 1.构建检索请求对象-包含请求体参数
     * 2.发送检索请求到ES-ES处理接口响应结果
     * 3.接收ES响应结果，按照接口文档进行解析
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一、构建检索请求对象SearchRequest-封装检索请求以及请求体参数
            SearchRequest searchRequest = this.buildDSL(searchParam);

            //二、调用SDK执行检索
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三、解析响应结果
            return this.parseResult(response, searchParam);
        } catch (Exception e) {
            log.error("[搜索服务]检索商品异常：{}", e);
            throw new RuntimeException(e);
        }
    }


    /**
     * 构建检索请求对象（构建发起请求DSL语句）封装请求路径以及请求参数
     *
     * @param searchParam 查询条件
     * @return
     */
    @Override
    public SearchRequest buildDSL(SearchParam searchParam) {
        //1.创建检索请求对象SearchReqeust 封装请求路径，请求方式，要检索索引库  GET goods/_search
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        //2.创建请求体参数对象SearchSourceBuilder 封装请求体所有请求参数 包括：查询方式、字段指定、分页、高亮、排序、聚合等
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //2.1 封装“查询方式”设置请求体参数中"query"属性 包含用户填写，选择的各种查询条件
        //2.1.1 创建封装所有查询条件布尔条件对象
        BoolQueryBuilder allBoolQueryBuilder = QueryBuilders.boolQuery();

        //2.1.2 在布尔条件对象中封装must必须满足条件：关键字匹配/全文检索查询
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            allBoolQueryBuilder.must(QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND));
        }
        //2.1.3 在布尔条件对象中封装filter各种过滤条件
        //2.1.3.1 在filter中设置品牌过滤 品牌过滤提交trademark参数值---3:华为   品牌ID:品牌名称
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {
            String[] split = searchParam.getTrademark().split(":");
            if (split != null && split.length == 2) {
                allBoolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.3.2 在filter中设置分类过滤 可能存在1,2,3级分类ID
        if (searchParam.getCategory1Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id()));
        }
        if (searchParam.getCategory2Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id()));
        }
        if (searchParam.getCategory3Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id()));
        }
        //2.1.3.3 在filter中设置平台属性过滤 可能存在多个平台属性过滤条件 参数值形式  平台属性ID:属性值名称:属性名称
        if (searchParam.getProps() != null && searchParam.getProps().length > 0) {
            // 循环处理每组平台属性过滤条件-每处理一个设置nested平台属性过滤
            for (String prop : searchParam.getProps()) {
                //2.1.4 创建平台属性nested查询对象
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    //2.1.5 创建一组平台属性过滤条件：构建布尔查询
                    BoolQueryBuilder attrsBoolQueryBuilder = QueryBuilders.boolQuery();
                    //2.1.5.1 在平台属性布尔查询中设置平台属性ID条件
                    attrsBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    //2.1.5.2 在平台属性布尔查询中设置平台属性值条件
                    attrsBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    NestedQueryBuilder attrsNestedQueryBuilder = QueryBuilders.nestedQuery("attrs", attrsBoolQueryBuilder, ScoreMode.None);
                    //2.1.6 将平台属性过滤条件nested加入到最大的布尔查询条件中
                    allBoolQueryBuilder.filter(attrsNestedQueryBuilder);
                }
            }
        }
        searchSourceBuilder.query(allBoolQueryBuilder);

        //2.2 封装“分页”条件，设置请求体参数中"from","size"属性 包含：起始位置索引、页大小
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        int from = (pageNo - 1) * pageSize;
        searchSourceBuilder.from(from).size(pageSize);

        //2.3 封装“指定响应字段列表”，设置请求体参数中"_source"属性 包含：ES响应业务参数字段
        searchSourceBuilder.fetchSource(new String[]{"id", "title", "defaultImg", "price"}, null);

        //2.4 封装“高亮显示关键字”，设置请求体参数中"highlight"属性 包含：高亮字段、高亮HTML标签
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.preTags("<font style='color:red'>");
            highlightBuilder.postTags("</font>");
            highlightBuilder.field("title");
            searchSourceBuilder.highlighter(highlightBuilder);
        }

        //2.5 封装“排序字段”，设置请求体参数中"sort"属性 包含：排序字段、排序方式 默认：按照相关度得分排序
        if (StringUtils.isNotBlank(searchParam.getOrder())) {
            String orderField = "";
            String[] split = searchParam.getOrder().split(":");
            if (split != null && split.length == 2) {
                if ("1".equals(split[0])) {
                    orderField = "hotScore";
                }
                if ("2".equals(split[0])) {
                    orderField = "price";
                }
            }
            searchSourceBuilder.sort(orderField, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
        }

        //2.6 封装“聚合字段”，设置请求体参数中"aggs"属性 包含：对品牌、平台属性聚合 每个聚合包含：聚合名称、聚合字段、聚合类型
        //2.6.1 设置品牌聚合
        //2.6.1.1 创建品牌ID聚合对象 聚合三要素：聚合名称：tmIdAgg .term()方法聚合方式：terms  .field()方法 聚合字段：tmId
        TermsAggregationBuilder tmIdAgg = AggregationBuilders.terms("tmIdAgg").field("tmId");
        //2.6.1.2 基于品牌ID聚合对象创建 品牌名称 子聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"));
        //2.6.1.3 基于品牌ID聚合对象创建 品牌Logo 子聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmLogoAgg").field("tmLogoUrl"));
        searchSourceBuilder.aggregation(tmIdAgg);

        //2.6.2 设置平台属性聚合 关键：attrs字段类型nested
        //2.6.2.1.创建nested平台属性聚合对象
        NestedAggregationBuilder attrsNested = AggregationBuilders.nested("attrsAgg", "attrs");
        //2.6.2.2 创建平台属性id聚合对象-放入到平台属性聚合对象中
        TermsAggregationBuilder attrsIdAgg = AggregationBuilders.terms("attrsIdAgg").field("attrs.attrId");
        //2.6.2.3 基于平台属性id聚合创建 平台属性名称 子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsNameAgg").field("attrs.attrName"));
        //2.6.2.4 基于平台属性id聚合创建 平台属性值 子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsValueAgg").field("attrs.attrValue"));
        //平台属性id聚合对象 放入到平台属性聚合对象中
        attrsNested.subAggregation(attrsIdAgg);
        searchSourceBuilder.aggregation(attrsNested);

        //3.将请求体参数封装到请求对象中
        //System.out.println("本次检索DSL语句中请求体参数：");
        //System.err.println(searchSourceBuilder.toString());
        return searchRequest.source(searchSourceBuilder);
    }


    /**
     * 解析结果：将ES响应结果按照接口文档（前端）要求响应VO对象
     *
     * @param response
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam) {
        SearchResponseVo vo = new SearchResponseVo();

        //1.封装vo中的分页信息
        //1.1 从入参中获取页码、页大小
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        vo.setPageNo(pageNo);
        vo.setPageSize(pageSize);
        //1.2 从ES响应结果中获取总记录数
        long total = response.getHits().getTotalHits().value;
        vo.setTotal(total);
        //1.3 通过总记录数、页大小 计算总页数
        long totalPage = total % pageSize == 0 ? (total / pageSize) : (total / pageSize) + 1;
        vo.setTotalPages(totalPage);

        //2.封装vo中的当前页检索到商品列表（处理高亮字段）
        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();
        if (searchHits != null && searchHits.length > 0) {
            // 2.1 将数组转为集合List
            List<SearchHit> searchHitsList = Arrays.asList(searchHits);
            // 2.2 遍历命中业务数据 将业务数据JSON转为Java对象
            List<Goods> goodsList = searchHitsList.stream().map(searchHit -> {
                String goodsSourceJSON = searchHit.getSourceAsString();
                Goods goods = JSON.parseObject(goodsSourceJSON, Goods.class);
                // 2.3 处理高亮
                Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                if (!CollectionUtils.isEmpty(highlightFields)) {
                    // 2.3.1 获取高亮对象
                    HighlightField highlightField = highlightFields.get("title");
                    // 2.3.2 获取高亮片段（字符串）
                    String titleHightLight = highlightField.getFragments()[0].toString();
                    goods.setTitle(titleHightLight);
                }
                return goods;
            }).collect(Collectors.toList());
            vo.setGoodsList(goodsList);
        }

        //3.0 获取结果中所有聚合-将聚合结果转为Map
        Map<String, Aggregation> allAggregationMap = response.getAggregations().asMap();
        //3.封装vo中聚合到品牌列表集合
        //问题：Aggregation接口没有提供获取聚合结果“桶”方法
        //解决：Aggregation->MultiBucketsAggregation->Terms->ParsedTerms->ParsedStringTerms/ParsedLongTerms
        //根据当时设置聚合字段类型决定选择哪个实现类
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        List<? extends Terms.Bucket> buckets = tmIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(buckets)) {
            List<SearchResponseTmVo> tmVoList = buckets.stream().map(bucket -> {
                //3.1 遍历聚合到品牌桶，得到当前品牌ID
                long tmId = bucket.getKeyAsNumber().longValue();
                //3.2 获取品牌名称子聚合结果对象，得到品牌名称(该名称结果一定只有一个)
                ParsedStringTerms tmNameAgg = bucket.getAggregations().get("tmNameAgg");
                String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();

                //3.3 获取品牌Logo子聚合结果对象，得到品牌Logo(品牌Logo一定只有一个)，
                ParsedStringTerms tmLogoAgg = bucket.getAggregations().get("tmLogoAgg");
                String tmLogoUrl = tmLogoAgg.getBuckets().get(0).getKeyAsString();

                //3.3 封装为SearchResponseTmVo对象
                SearchResponseTmVo tmVo = new SearchResponseTmVo();
                tmVo.setTmId(tmId);
                tmVo.setTmName(tmName);
                tmVo.setTmLogoUrl(tmLogoUrl);
                return tmVo;
            }).collect(Collectors.toList());
            vo.setTrademarkList(tmVoList);
        }

        //4.封装vo中聚合到平台属性列表集合
        //4.1 获取平台属性nested聚合结果对象
        ParsedNested attrsAgg = (ParsedNested) allAggregationMap.get("attrsAgg");
        //4.2 基于平台属性nested聚合结果对象获取平台属性ID子聚合结果
        ParsedLongTerms attrsIdAgg = attrsAgg.getAggregations().get("attrsIdAgg");
        List<? extends Terms.Bucket> attrIdBuckets = attrsIdAgg.getBuckets();
        if(!CollectionUtils.isEmpty(attrIdBuckets)){
            List<SearchResponseAttrVo> attrVoList = attrIdBuckets.stream().map(bucket -> {
                //获取聚合到平台属性ID
                long attrId = bucket.getKeyAsNumber().longValue();
                //4.2.1 基于平台属性ID聚合结果，获取平台属性名称子聚合结果
                ParsedStringTerms attrsNameAgg = bucket.getAggregations().get("attrsNameAgg");
                String attrName = attrsNameAgg.getBuckets().get(0).getKeyAsString();

                //4.2.2 基于平台属性ID聚合结果，获取平台属性值子聚合结果
                ParsedStringTerms attrsValueAgg = bucket.getAggregations().get("attrsValueAgg");
                List<String> attrValueList = attrsValueAgg.getBuckets()
                        .stream().map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());

                //4.2.3 封装平台属性VO对象
                SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                attrVo.setAttrId(attrId);
                attrVo.setAttrName(attrName);
                attrVo.setAttrValueList(attrValueList);
                return attrVo;
            }).collect(Collectors.toList());
            vo.setAttrsList(attrVoList);
        }
        return vo;
    }

}
